﻿/*
 * This file is part of CommunitySettlers.
 *
 * CommunitySettlers is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.

 * CommunitySettlers is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.

 * You should have received a copy of the GNU General Public License
 * along with CommunitySettlers.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * Author: Christoph Husse
 * 
 * Also checkout our homepage: http://communitysettlers.codeplex.com/
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;

namespace SettlersEngine
{
    internal enum Command : byte
    {
        Header,
        RemovePositionProvider,
        AddResourceProvider,
        AddResourceQuery,
        AddResourceDrop,
        AddBuilding,
        AddStatic,
        AddMoveable,
        AddStaticProvider,
        SetMovablePath,
        RequestPathCompletion,
    }

    internal enum CommandParamType : byte
    {
        Byte,
        Int32,
        Int64,
        Double,
        Point,
        Guid,
        GuidArray,
    }

    internal class CommandInfo
    {
        public Command Command { get; internal set; }
        public Object[] Params { get; internal set; }

        internal CommandInfo()
        {
        }
    }


    internal class NetworkProtocol
    {
        private readonly List<CommandInfo> m_Commands = new List<CommandInfo>();
        private readonly MemoryStream m_PacketStream = new MemoryStream();
        private Int32 m_PacketIndex = 0;

        internal static Int32 GetCommandCount(byte[] inData)
        {
            int packetIndex;

            List<CommandInfo>[] commandLists = NetworkProtocol.Unpack(inData, out packetIndex);

            // TODO: validate integrity

            if (commandLists.Length != 1)
                throw new InvalidDataException();

            return commandLists[0].Count;
        }

        internal static void Stamp(Guid inPlayerUID, Int64 inSyncTime, Int64 inSyncCounter, byte[] inData)
        {
            MemoryStream stream = new MemoryStream(inData);
            BinaryWriter writer = new BinaryWriter(stream);

            stream.Position = 4;
            writer.Write((Int64)inSyncTime);
        }

        public void Clear()
        {
            lock (m_Commands)
            {
                m_Commands.Clear();
            }
        }

        public void Execute(Command inCommand, params Object[] inParams)
        {
            lock (m_Commands)
            {
                m_Commands.Add(new CommandInfo()
                {
                    Command = inCommand,
                    Params = inParams,
                });
            }
        }

        public List<byte[]> Pack(Guid inPlayerID)
        {
            return Pack(inPlayerID, 0);
        }

        public List<byte[]> Pack(Guid inPlayerID, Int64 inServerTime)
        {
            List<List<CommandInfo>> commandLists = new List<List<CommandInfo>>();
            List<byte[]> packets = new List<byte[]>();

            // derive masked player id
            byte[] masked = inPlayerID.ToByteArray();

            for (int i = 0; i < 8; i++)
            {
                masked[i] = 0;
            }

            Guid maskedID = new Guid(masked);

            lock (m_Commands)
            {
                if (m_Commands.Count == 0)
                    return packets;

                for(int i = 0; i < m_Commands.Count; )
                {
                    List<CommandInfo> commands = new List<CommandInfo>();

                    commandLists.Add(commands);

                    // insert header
                    commands.Add(new CommandInfo()
                    {
                        Command = Command.Header,
                        Params = new Object[] { maskedID, (Int64)inServerTime },
                    });

                    for (int x = 0; (i < m_Commands.Count) && (x < 100); i++, x++)
                    {
                        commands.Add(m_Commands[i]);
                    }

                }

                m_Commands.Clear();
            }

            BinaryWriter writer = new BinaryWriter(m_PacketStream);

            foreach (var commands in commandLists)
            {
                int packetIndex = System.Threading.Interlocked.Increment(ref m_PacketIndex);

                m_PacketStream.SetLength(0);
                writer.Write((Int32)packetIndex);
                writer.Write((Int64)0); // placeholder for server timestamp
                writer.Write((Int32)commands.Count);

                // write packet content
                foreach(var cmd in commands)
                {
                    writer.Write((Byte)cmd.Command);
                    writer.Write((Byte)cmd.Params.Length);

                    foreach (var param in cmd.Params)
                    {
                        if (param is System.Drawing.Point)
                        {
                            System.Drawing.Point point = (System.Drawing.Point)param;
                            writer.Write((Byte)CommandParamType.Point);
                            writer.Write((Int32)point.X);
                            writer.Write((Int32)point.Y);
                        }
                        else if (param is Byte)
                        {
                            writer.Write((Byte)CommandParamType.Byte);
                            writer.Write((Byte)param);
                        }
                        else if (param is Int32)
                        {
                            writer.Write((Byte)CommandParamType.Int32);
                            writer.Write((Int32)param);
                        }
                        else if (param is Int64)
                        {
                            writer.Write((Byte)CommandParamType.Int64);
                            writer.Write((Int64)param);
                        }
                        else if (param is Double)
                        {
                            writer.Write((Byte)CommandParamType.Double);
                            writer.Write((Double)param);
                        }
                        else if (param is Guid)
                        {
                            writer.Write((Byte)CommandParamType.Guid);
                            writer.Write(((Guid)param).ToByteArray());
                        }
                        else if (param is Guid[])
                        {
                            Guid[] guids = (Guid[])param;

                            writer.Write((Byte)CommandParamType.GuidArray);
                            writer.Write((Byte)guids.Length);

                            foreach (Guid guid in guids)
                            {
                                writer.Write(guid.ToByteArray());
                            }
                        }
                        else
                            throw new ApplicationException("Command parameter type \"" + param.GetType().FullName + "\" is not supported!");
                    }
                }

                packets.Add(m_PacketStream.ToArray());
            }

            return packets;
        }

        internal static List<CommandInfo>[] Unpack(byte[] inData, out Int32 outPacketIndex)
        {
            List<List<CommandInfo>> result = new List<List<CommandInfo>>();
            MemoryStream packetStream = new MemoryStream(inData);
            BinaryReader reader = new BinaryReader(packetStream);

            outPacketIndex = -1;

            while (packetStream.Position < packetStream.Length)
            {
                outPacketIndex = reader.ReadInt32();
                Int64 serverTime = reader.ReadInt64();

                int commandCount = reader.ReadInt32();
                List<CommandInfo> commands = new List<CommandInfo>();

                for (int i = 0; i < commandCount; i++)
                {
                    CommandInfo cmd = new CommandInfo();

                    cmd.Command = (Command)reader.ReadByte();
                    cmd.Params = new object[reader.ReadByte()];

                    for(int iParam = 0; iParam < cmd.Params.Length; iParam++)
                    {
                        switch ((CommandParamType)reader.ReadByte())
                        {
                            case CommandParamType.Int32:
                                {
                                    cmd.Params[iParam] = reader.ReadInt32();
                                }break;
                            case CommandParamType.Int64:
                                {
                                    cmd.Params[iParam] = reader.ReadInt64();
                                } break;
                            case CommandParamType.Double:
                                {
                                    cmd.Params[iParam] = reader.ReadDouble();
                                } break;
                            case CommandParamType.Point:
                                {
                                    int x = reader.ReadInt32();
                                    int y = reader.ReadInt32();

                                    cmd.Params[iParam] = new System.Drawing.Point(x, y);
                                } break;
                            case CommandParamType.Byte:
                                {
                                    cmd.Params[iParam] = reader.ReadByte();
                                } break;
                            case CommandParamType.Guid:
                                {
                                    cmd.Params[iParam] = new Guid(reader.ReadBytes(16));
                                }break;
                            case CommandParamType.GuidArray:
                                {
                                    Guid[] guids = new Guid[reader.ReadByte()];

                                    for (int iGuid = 0; iGuid < guids.Length; iGuid++)
                                    {
                                        guids[iGuid] = new Guid(reader.ReadBytes(16));
                                    }

                                    cmd.Params[iParam] = guids;
                                } break;
                            default:
                                throw new InvalidDataException("Unknown command parameter type.");
                        }
                    }

                    if (cmd.Command == Command.Header)
                    {
                        cmd.Params[1] = serverTime;
                    }

                    commands.Add(cmd);
                }

                result.Add(commands);
            }

            return result.ToArray();
        }
    }
}